package com.ks.common.util;

import io.minio.*;
import io.minio.http.Method;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.web.multipart.MultipartFile;

import java.io.InputStream;
import java.net.URLDecoder;
import java.util.HashMap;
import java.util.Iterator;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;


/**
 * MinIO工具类
 *
 * @author KingAusten
 * @date 2022/08/18 14:33:26
 */
@Slf4j
@Data
@EqualsAndHashCode(callSuper = false)
public class MinioUtil{

    /** 初始化文件头类型，不够的自行补充 */
    final static HashMap<String,String> FILE_TYPE_MAP = new HashMap<>();

    /** minio url */
    private static String minioUrl;

    /** minio名字 */
    private static String minioName;

    /** minio密码 */
    private static String minioPass;

    /**
     * update-begin-author:kingausten for:
     */
    private static String bucketName;

    /** minio客户端 */
    private static MinioClient minioClient = null;

    /** 不允许上传文件后缀 */
    private static String[] forbidType = {"jsp",
            "php"};

    /**
     * 文件名 正则字符串
     * 文件名支持的字符串：字母数字中文.-_()（） 除此之外的字符将被删除
     */
    private static String FILE_NAME_REGEX = "[^A-Za-z\\.\\(\\)\\-（）\\_0-9\\u4e00-\\u9fa5]";

    static{
        FILE_TYPE_MAP.put("3c25402070616765206c","jsp");
        FILE_TYPE_MAP.put("3c3f7068700a0a2f2a2a0a202a205048","php");
       /* fileTypeMap.put("ffd8ffe000104a464946", "jpg");
        fileTypeMap.put("89504e470d0a1a0a0000", "png");
        fileTypeMap.put("47494638396126026f01", "gif");
        fileTypeMap.put("49492a00227105008037", "tif");
        fileTypeMap.put("424d228c010000000000", "bmp");
        fileTypeMap.put("424d8240090000000000", "bmp");
        fileTypeMap.put("424d8e1b030000000000", "bmp");
        fileTypeMap.put("41433130313500000000", "dwg");
        fileTypeMap.put("3c21444f435459504520", "html");
        fileTypeMap.put("3c21646f637479706520", "htm");
        fileTypeMap.put("48544d4c207b0d0a0942", "css");
        fileTypeMap.put("696b2e71623d696b2e71", "js");
        fileTypeMap.put("7b5c727466315c616e73", "rtf");
        fileTypeMap.put("38425053000100000000", "psd");
        fileTypeMap.put("46726f6d3a203d3f6762", "eml");
        fileTypeMap.put("d0cf11e0a1b11ae10000", "doc");
        fileTypeMap.put("5374616E64617264204A", "mdb");
        fileTypeMap.put("252150532D41646F6265", "ps");
        fileTypeMap.put("255044462d312e350d0a", "pdf");
        fileTypeMap.put("2e524d46000000120001", "rmvb");
        fileTypeMap.put("464c5601050000000900", "flv");
        fileTypeMap.put("00000020667479706d70", "mp4");
        fileTypeMap.put("49443303000000002176", "mp3");
        fileTypeMap.put("000001ba210001000180", "mpg");
        fileTypeMap.put("3026b2758e66cf11a6d9", "wmv");
        fileTypeMap.put("52494646e27807005741", "wav");
        fileTypeMap.put("52494646d07d60074156", "avi");
        fileTypeMap.put("4d546864000000060001", "mid");
        fileTypeMap.put("504b0304140000000800", "zip");
        fileTypeMap.put("526172211a0700cf9073", "rar");
        fileTypeMap.put("235468697320636f6e66", "ini");
        fileTypeMap.put("504b03040a0000000000", "jar");
        fileTypeMap.put("4d5a9000030000000400", "exe");
        fileTypeMap.put("3c25402070616765206c", "jsp");
        fileTypeMap.put("4d616e69666573742d56", "mf");
        fileTypeMap.put("3c3f786d6c2076657273", "xml");
        fileTypeMap.put("494e5345525420494e54", "sql");
        fileTypeMap.put("7061636b616765207765", "java");
        fileTypeMap.put("406563686f206f66660d", "bat");
        fileTypeMap.put("1f8b0800000000000000", "gz");
        fileTypeMap.put("6c6f67346a2e726f6f74", "properties");
        fileTypeMap.put("cafebabe0000002e0041", "class");
        fileTypeMap.put("49545346030000006000", "chm");
        fileTypeMap.put("04000000010000001300", "mxp");
        fileTypeMap.put("504b0304140006000800", "docx");
        fileTypeMap.put("6431303a637265617465", "torrent");
        fileTypeMap.put("6D6F6F76", "mov");
        fileTypeMap.put("FF575043", "wpd");
        fileTypeMap.put("CFAD12FEC5FD746F", "dbx");
        fileTypeMap.put("2142444E", "pst");
        fileTypeMap.put("AC9EBD8F", "qdf");
        fileTypeMap.put("E3828596", "pwl");
        fileTypeMap.put("2E7261FD", "ram");*/
    }

    /**
     * 上传文件
     *
     * @param file
     *
     * @return
     */
    public static String upload(MultipartFile file,String bizPath){

        String fileUrl = "";
        bizPath = MinioUtil.filter(bizPath);
        String newBucket = bucketName;
        try{
            initMinio(minioUrl,minioName,minioPass);
            // 检查存储桶是否已经存在
            if(minioClient.bucketExists(BucketExistsArgs.builder()
                                                .bucket(newBucket)
                                                .build())){
                log.info("Bucket already exists.");
            }else{
                // 创建一个名为ota的存储桶
                minioClient.makeBucket(MakeBucketArgs.builder()
                                               .bucket(newBucket)
                                               .build());
                log.info("create a new bucket.");
            }
            //过滤上传文件类型
            MinioUtil.fileTypeFilter(file);
            //过滤上传文件类型
            InputStream stream = file.getInputStream();
            // 获取文件名
            String orgName = file.getOriginalFilename();
            if("".equals(orgName)){
                orgName = file.getName();
            }
            orgName = MinioUtil.getFileName(orgName);
            String objectName = bizPath + "/" + (! orgName.contains(".") ?
                                                 orgName + "_" + System.currentTimeMillis() :
                                                 orgName.substring(0,orgName.lastIndexOf(".")) + "_" + System.currentTimeMillis() + orgName.substring(orgName.lastIndexOf(".")));

            // 使用putObject上传一个本地文件到存储桶中。
            if(objectName.startsWith("/")){
                objectName = objectName.substring(1);
            }
            PutObjectArgs objectArgs = PutObjectArgs.builder()
                    .object(objectName)
                    .bucket(newBucket)
                    .contentType("application/octet-stream")
                    .stream(stream,stream.available(),- 1)
                    .build();
            minioClient.putObject(objectArgs);
            stream.close();
            fileUrl = minioUrl + newBucket + "/" + objectName;
        }
        catch(Exception e){
            log.error(e.getMessage(),e);
        }
        return fileUrl;
    }

    /**
     * 过滤器
     * <p>
     * 清除非法字符
     * </p>
     *
     * @param str str
     *
     * @return {@link String}
     *
     * @throws PatternSyntaxException 模式语法例外
     */
    public static String filter(String str) throws PatternSyntaxException{
        // 清除掉所有特殊字符
        String regEx = "[`_《》~!@#$%^&*()+=|{}':;',\\[\\].<>?~！@#￥%……&*（）——+|{}【】‘；：”“’。，、？]";
        Pattern p = Pattern.compile(regEx);
        Matcher m = p.matcher(str);
        return m.replaceAll("")
                .trim();
    }

    /**
     * 初始化客户端
     *
     * @param minioUrl  minio 地址
     * @param minioName minio 用户
     * @param minioPass minio 密码
     *
     * @return
     */
    private static MinioClient initMinio(String minioUrl,String minioName,String minioPass){

        if(minioClient == null){
            try{
                minioClient = MinioClient.builder()
                        .endpoint(minioUrl)
                        .credentials(minioName,minioPass)
                        .build();
            }
            catch(Exception e){
                e.printStackTrace();
            }
        }
        return minioClient;
    }

    /**
     * 文件类型过滤
     *
     * @param file
     */
    public static void fileTypeFilter(MultipartFile file) throws Exception{

        String suffix = getFileType(file);
        for(String type: forbidType){
            if(type.contains(suffix)){
                throw new Exception("上传失败，文件类型异常：" + suffix);
            }
        }
    }

    /**
     * 判断文件名是否带盘符，重新处理
     *
     * @param fileName
     *
     * @return
     */
    public static String getFileName(String fileName){
        //判断是否带有盘符信息
        // Check for Unix-style path
        int unixSep = fileName.lastIndexOf('/');
        // Check for Windows-style path
        int winSep = fileName.lastIndexOf('\\');
        // Cut off at latest possible point
        int pos = (Math.max(winSep,unixSep));
        if(pos != - 1){
            fileName = fileName.substring(pos + 1);
        }
        //替换上传文件名字的特殊字符
        fileName = fileName.replace("=","")
                .replace(",","")
                .replace("&","")
                .replace("#","")
                .replace("“","")
                .replace("”","");
        //替换上传文件名字中的空格
        fileName = fileName.replaceAll("\\s","");
        fileName = fileName.replaceAll(FILE_NAME_REGEX,"");
        return fileName;
    }

    /**
     * 通过读取文件头部获得文件类型
     *
     * @param file
     *
     * @return 文件类型
     *
     * @throws Exception
     */
    private static String getFileType(MultipartFile file) throws Exception{

        String fileExtendName = null;
        InputStream is;
        try{
            //is = new FileInputStream(file);
            is = file.getInputStream();
            byte[] b = new byte[10];
            is.read(b,0,b.length);
            String fileTypeHex = String.valueOf(bytesToHexString(b));
            Iterator<String> keyIter = FILE_TYPE_MAP.keySet()
                    .iterator();
            while(keyIter.hasNext()){
                String key = keyIter.next();
                // 验证前5个字符比较
                if(key.toLowerCase()
                        .startsWith(fileTypeHex.toLowerCase()
                                            .substring(0,5)) || fileTypeHex.toLowerCase()
                        .substring(0,5)
                        .startsWith(key.toLowerCase())){
                    fileExtendName = FILE_TYPE_MAP.get(key);
                    break;
                }
            }
            // 如果不是上述类型，则判断扩展名
            if(StringUtils.isBlank(fileExtendName)){
                String fileName = file.getOriginalFilename();
                return getFileTypeBySuffix(fileName);
            }
            is.close();
            return fileExtendName;
        }
        catch(Exception exception){
            throw new Exception(exception.getMessage(),exception);
        }
    }

    /**
     * 获得文件头部字符串
     *
     * @param src
     *
     * @return
     */
    private static String bytesToHexString(byte[] src){

        StringBuilder stringBuilder = new StringBuilder();
        if(src == null || src.length <= 0){
            return null;
        }
        for(int i = 0;i < src.length;i++){
            int v = src[i] & 0xFF;
            String hv = Integer.toHexString(v);
            if(hv.length() < 2){
                stringBuilder.append(0);
            }
            stringBuilder.append(hv);
        }
        return stringBuilder.toString();
    }

    /**
     * @param fileName
     *
     * @return String
     *
     * @description 通过文件后缀名获取文件类型
     */
    private static String getFileTypeBySuffix(String fileName){

        return fileName.substring(fileName.lastIndexOf(".") + 1,fileName.length());
    }

    /**
     * 获取文件流
     *
     * @param bucketName
     * @param objectName
     *
     * @return
     */
    public static InputStream getMinioFile(String bucketName,String objectName){

        InputStream inputStream = null;
        try{
            initMinio(minioUrl,minioName,minioPass);
            GetObjectArgs objectArgs = GetObjectArgs.builder()
                    .object(objectName)
                    .bucket(bucketName)
                    .build();
            inputStream = minioClient.getObject(objectArgs);
        }
        catch(Exception e){
            log.info("文件获取失败" + e.getMessage());
        }
        return inputStream;
    }

    /**
     * 删除文件
     *
     * @param bucketName
     * @param objectName
     *
     * @throws Exception
     */
    public static void removeObject(String bucketName,String objectName){

        try{
            initMinio(minioUrl,minioName,minioPass);
            RemoveObjectArgs objectArgs = RemoveObjectArgs.builder()
                    .object(objectName)
                    .bucket(bucketName)
                    .build();
            minioClient.removeObject(objectArgs);
        }
        catch(Exception e){
            log.info("文件删除失败" + e.getMessage());
        }
    }

    /**
     * 获取文件外链
     *
     * @param bucketName
     * @param objectName
     * @param expires
     *
     * @return
     */
    public static String getObjectUrl(String bucketName,String objectName,Integer expires){

        initMinio(minioUrl,minioName,minioPass);
        try{
            //获取文件外链报错提示method不能为空，导致文件下载和预览失败----
            GetPresignedObjectUrlArgs objectArgs = GetPresignedObjectUrlArgs.builder()
                    .object(objectName)
                    .bucket(bucketName)
                    .expiry(expires)
                    .method(Method.GET)
                    .build();
            //获取文件外链报错提示method不能为空，导致文件下载和预览失败----
            String url = minioClient.getPresignedObjectUrl(objectArgs);
            return URLDecoder.decode(url,"UTF-8");
        }
        catch(Exception e){
            log.info("文件路径获取失败" + e.getMessage());
        }
        return null;
    }

}